import * as vscode from 'vscode';
import axios from 'axios';
import { Buffer } from 'buffer';
import { debug } from './logger';
import { MagicFileInfo, MagicGroupInfo } from './magicFileSystemProvider';
import { MagicResourceType } from './types';

export interface MagicServerConfig {
    id: string;
    name: string;
    url: string;
    username?: string;
    password?: string;
    token?: string;
    lspPort?: number;
    debugPort?: number;
}

export interface CreateFileRequest {
    name: string;
    script: string;
    // 例如: "api/user" 或 "function/util"
    groupPath?: string; // 例如: "api/user" 或 "function/util"
    groupId?: string | null; // 兼容旧接口，优先使用 groupPath
    type: MagicResourceType;
    method?: string;
    requestMapping?: string;
    description?: string;
}

export interface CreateGroupRequest {
    name: string;
    parentId: string | null;
    type: MagicResourceType;
    description?: string;
}

// 原始分组导出结构（用于写入 .group.meta.json）
export interface MagicGroupMetaRaw {
    properties?: Record<string, any>;
    id?: string;
    name?: string;
    type?: MagicResourceType | string;
    parentId?: string;
    path?: string; // 分组 URL 路由片段（可能带前导 /）
    createTime?: number;
    updateTime?: number;
    createBy?: string;
    updateBy?: string;
    paths?: any[];
    options?: any[];
}

export class MagicApiClient {
    private exposeHeaders: string = 'magic-token';
    private httpClient: any;
    private pathToIdCache = new Map<string, string>();
    private idToPathCache = new Map<string, string>();
    private parentToChildrenCache?: Map<string, string[]>;
    private webPrefix: string;
    private sessionToken?: string;
    private loginInFlight?: Promise<string | null>;
    private resourceTreeCache?: { data: any; ts: number };
    private resourceMapCache?: Map<string, any>;
    // 最近一次服务端错误信息，用于在调用处展示更详细的错误提示
    private lastErrorMessage?: string;
    private lastErrorCode?: number;

    private getExtForType(type: MagicResourceType): string {
        return type === 'datasource' ? '.json' : '.ms';
    }

    constructor(private config: MagicServerConfig) {
        const base = new URL(config.url);
        const basePath = base.pathname.replace(/\/$/, '');
        this.webPrefix = `${base.origin}${basePath}`;
        this.httpClient = axios.create({
            baseURL: this.webPrefix,
            timeout: 15000,
            headers: { 'Content-Type': 'application/json' },
        });
        this.httpClient.interceptors.request.use((cfg: any) => {
            const tok = this.sessionToken || this.config.token;
            if (tok) {
                cfg.headers = cfg.headers || {};
                cfg.headers[this.exposeHeaders] = tok;
            }
            return cfg;
        });

        // 响应拦截器：同时在成功分支与错误分支处理鉴权失效（code=401 或消息提示）并自动登录重试
        this.httpClient.interceptors.response.use(
            async (response: any) => {
                try {
                    const body =  response?.data;
                    const code = typeof body?.code !== 'undefined' ? body.code : undefined;
                    const message = body?.message || '';
                    const path = response?.config?.url;
                    const isTokenInvalid = code === 401 || /token无效/i.test(String(message));
                    if (isTokenInvalid && response?.config && !response.config._retry) {
                        response.config._retry = true;
                        debug(`HTTP 200 但业务码为401/token无效: url=${path ?? 'n/a'}，尝试自动登录并重试`);
                        const token = await this.ensureLogin();
                        if (token) {
                            response.config.headers = response.config.headers || {};
                            response.config.headers[this.exposeHeaders] = token;
                            try {
                                return await this.httpClient.request(response.config);
                            } catch (retryErr: any) {
                                const rStatus = retryErr?.response?.status;
                                debug(`重试失败: status=${rStatus ?? 'n/a'} url=${path ?? 'n/a'} message=${retryErr?.message}`);
                                vscode.window.showErrorMessage(`请求重试失败: ${retryErr?.message}`);
                                return Promise.reject(retryErr);
                            }
                        }
                    }
                } catch (e) {
                    // 安全兜底：若解析失败则直接返回原响应
                }
                return response;
            },
            (error: any) => error
        )
    }

    // 错误信息管理：供调用方展示详细服务端信息
    private setLastError(message?: string, code?: number) {
        this.lastErrorMessage = message || undefined;
        this.lastErrorCode = typeof code === 'number' ? code : undefined;
    }

    private clearLastError() {
        this.lastErrorMessage = undefined;
        this.lastErrorCode = undefined;
    }

    public getLastErrorMessage(): string | undefined {
        return this.lastErrorMessage;
    }

    getAuthHeaders(): Record<string, string> {
        const tok = this.sessionToken || this.config.token;
        return tok ? { [this.exposeHeaders]: tok } : {};
    }

    async ensureLogin(): Promise<string | null> {
        if (this.sessionToken) return this.sessionToken;
        if (this.loginInFlight) return this.loginInFlight;
        this.loginInFlight = this.login();
        const tok = await this.loginInFlight.catch(() => null);
        this.loginInFlight = undefined;
        return tok;
    }
    
    // 使用用户名密码自动登录并获取令牌（兼容多种返回结构与路径）
    private async login(): Promise<string | null> {
        const { username, password } = this.config;
        if (!username || !password) {
            debug('未配置用户名/密码，无法自动登录获取令牌');
            return null;
        }
        try {
            const axiosClient = axios.create({
                baseURL: this.webPrefix,
                timeout: 5000,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                },
            });
            const body = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`;
            const resp = await axiosClient.post('/login', body);
            const headers: any = resp?.headers || {};
            const getHeader = (name: string) => headers[name] || headers[name?.toLowerCase?.()] || headers[name?.toUpperCase?.()];
            const candidates = ['magic-token', 'authorization', 'x-auth-token'];
            let headerName: string | undefined;
            let token: string | undefined;
            for (const name of candidates) {
                const val = getHeader(name);
                if (typeof val === 'string' && val.length > 0) {
                    headerName = name;
                    token = val;
                    break;
                }
            }
            const bodyToken = (resp?.data as any)?.token || (resp?.data as any)?.data?.token;
            if (!token && typeof bodyToken === 'string' && bodyToken.length > 0) {
                token = bodyToken;
                headerName = 'magic-token';
            }
            if (token) {
                this.sessionToken = token;
                this.exposeHeaders = (headerName || this.exposeHeaders).toLowerCase();
                this.httpClient.defaults.headers.common = this.httpClient.defaults.headers.common || {};
                this.httpClient.defaults.headers.common[this.exposeHeaders] = token;
                debug(`登录成功，令牌已更新并附加到请求头: ${this.exposeHeaders}`);
                return token;
            }
            const expose = String(headers['access-control-expose-headers'] || headers['Access-Control-Expose-Headers'] || '').toLowerCase();
            if (expose.includes('magic-token')) {
                const tok2 = headers['magic-token'] || headers['Magic-Token'];
                if (typeof tok2 === 'string' && tok2.length > 0) {
                    this.sessionToken = tok2;
                    this.exposeHeaders = 'magic-token';
                    this.httpClient.defaults.headers.common[this.exposeHeaders] = tok2;
                    debug('登录成功（依据 expose-headers 提示），令牌已更新');
                    return tok2;
                }
            }
        } catch (e: any) {
            debug(`登录失败: ${String(e?.message || e)}`);
        }
        vscode.window.showErrorMessage('自动登录失败：未能获取令牌');
        return null;
    }

    public async getResourceTree(force: boolean = false): Promise<any> {
        await this.ensureLogin();
        const now = Date.now();
        const ttl = 5000; // 缓存 5s，减少 /resource 调用
        if (!force && this.resourceTreeCache && (now - this.resourceTreeCache.ts) < ttl) {
            return this.resourceTreeCache.data || {};
        }
        const resp = await this.httpClient.post('/resource');
        const tree = resp?.data?.data || {};
        this.resourceTreeCache = { data: tree, ts: now };
        const resourceMap = new Map<string, any>();
        const dfs = (
            nodeObj: any,
            type: string,
            urlPathArr: string[] = [],
            filePathArr: string[] = []
        ) => {
            if (!nodeObj) return;
            const n = nodeObj.node || {};
            // 赋值 type 字段
            n.type = type;

            // 拼接 fullUrlPath (拼接 path 字段)
            const nextUrlPathArr = [...urlPathArr];
            if (n.path) nextUrlPathArr.push(String(n.path));
            n.fullUrlPath = nextUrlPathArr.join('');

            // 拼接 fullFilePath (拼接 name 字段)
            const nextFilePathArr = [...filePathArr];
            if (n.name) nextFilePathArr.push(String(n.name === 'root' ? n.type : n.name));
            n.fullFilePath = nextFilePathArr.join('/');

            // 放入 resourceMap
            resourceMap.set(String(n.id || ''), n);

            // 遍历子节点
            const children: any[] = nodeObj.children || [];
            for (const child of children) {
                dfs(child, type, nextUrlPathArr, nextFilePathArr);
            }
        };

        Object.entries(tree).forEach(([type, root]: [string, any]) => {
            dfs(root, type);
        });
        this.resourceMapCache = resourceMap;
        this.pathToIdCache.clear();
        this.idToPathCache.clear();
        this.parentToChildrenCache = new Map<string, string[]>();
        const parentToChildren = this.parentToChildrenCache;
        const buildHierarchy = (nodeObj: any, parentId: string = '', t: string) => {
            if (!nodeObj) return;
            const n = nodeObj.node || {};
            const myId = String(n.id || '');
            const children = nodeObj.children || [];
            const childIds: string[] = [];
            for (const child of children) {
                const cId = String(child.node?.id || '');
                if (cId) {
                    childIds.push(cId);
                }
                buildHierarchy(child, myId, t);
            }
            if (childIds.length > 0) {
                parentToChildren.set(myId, childIds);
            }
            const rootKey = `root_${t}`;
            if (parentId === '' && childIds.length > 0) {
                parentToChildren.set(rootKey, childIds);
            }
        };
        Object.entries(tree).forEach(([t, root]: [string, any]) => {
            buildHierarchy(root, '', t);
        });
        // Build path caches from map
        for (const [idStr, n] of resourceMap.entries()) {
            const isFile = typeof n.groupId !== 'undefined';
            const ffp = String(n.fullFilePath || '');
            const type = String(n.type || '');
            if (isFile && n.id && type) {
                const fileName = String(n.name || '');
                const lastSlash = ffp.lastIndexOf('/');
                const gpName = lastSlash > -1 ? ffp.substring(0, lastSlash) : '';
                const ext = this.getExtForType(type as MagicResourceType);
                const fullKey = `${gpName}${gpName ? '/' : ''}${fileName}${ext}`;
                this.pathToIdCache.set(fullKey, idStr);
                this.idToPathCache.set(idStr, fullKey);
            } else if (!isFile && n.id && n.name && ffp && type) {
                const fullKey = `${ffp}`;
                this.pathToIdCache.set(fullKey, idStr);
                this.idToPathCache.set(idStr, fullKey);
            }
        }
        return tree;
    }

    async getResource(id: string): Promise<any | null> { 
        await this.ensureLogin();
        await this.getResourceTree();
        return this.resourceMapCache!.get(id) || null;
    }

    async getGroup(groupId: string): Promise<MagicGroupInfo | null> {
        const node = await this.getResource(groupId)
        if (node) {
            const n = node || {};
            const id = String(n.id || '');
            const name = String(n.name || '');
            const path = String(n.fullFilePath || '');
            const info: MagicGroupInfo = {
                id,
                name,
                path,
                parentId: n.parentId ? String(n.parentId) : undefined,
                type: (n.type) as MagicResourceType,
            };
            return info;
        }
        return null;
    }

    async getFiles(type: MagicResourceType, groupId: string | null): Promise<MagicFileInfo[]> {
        await this.ensureLogin();
        await this.getResourceTree();
        const typeStr = type;
        let childIds: string[] = [];
        let gpName = '';
        if (groupId) {
            const groupIdStr = String(groupId);
            childIds = this.parentToChildrenCache?.get(groupIdStr) || [];
            const groupNode = this.resourceMapCache?.get(groupIdStr);
            if (groupNode) {
                gpName = String(groupNode.fullFilePath || '');
            }
        } else {
            const rootKey = `root_${typeStr}`;
            childIds = this.parentToChildrenCache?.get(rootKey) || [];
            gpName = '';
            const typeRootId = `${typeStr}:0`;
            const typeRootChildren = this.parentToChildrenCache?.get(typeRootId);
            if (typeRootChildren && typeRootChildren.length > 0) {
                childIds = typeRootChildren;
                const typeRootNode = this.resourceMapCache?.get(typeRootId);
                if (typeRootNode) {
                    gpName = String(typeRootNode.fullFilePath || '');
                }
            }
        }
        const files: MagicFileInfo[] = [];
        for (const cid of childIds) {
            const n = this.resourceMapCache?.get(cid);
            if (!n || typeof n.groupId === 'undefined') continue;
            const info: MagicFileInfo = {
                id: String(n.id || ''),
                name: String(n.name || ''),
                type,
                groupId: String(n.groupId || ''),
                groupPath: `${typeStr}/${gpName}`,
                path: String(n.path || ''),
                requestMapping: String(n.requestMapping || ''),
                method: String(n.method || ''),
                description: String(n.description || ''),
                script: String(n.script || ''),
                createTime: n.createTime,
                updateTime: n.updateTime,
                createBy: n.createBy,
                updateBy: n.updateBy,
                // 从资源树缓存中获取预计算的完整 URL 路径
                fullUrlPath: n.fullUrlPath ? String(n.fullUrlPath) : undefined,
            };
            files.push(info);
        }
        return files;
    }

    async getGroups(type: MagicResourceType): Promise<MagicGroupInfo[]> {
        await this.ensureLogin();
        await this.getResourceTree();
        const groups: MagicGroupInfo[] = [];
        const typeStr = type;
        for (const [idStr, n] of this.resourceMapCache!.entries()) {
            if (n.type === typeStr && typeof n.groupId === 'undefined') {
                const path = String(n.fullFilePath || '');
                const info: MagicGroupInfo = {
                    id: idStr,
                    name: String(n.name || ''),
                    path,
                    parentId: n.parentId ? String(n.parentId) : undefined,
                    type,
                    createTime: n.createTime,
                    updateTime: n.updateTime,
                };
                groups.push(info);
            }
        }
        return groups;
    }

    async getFile(fileId: string): Promise<MagicFileInfo | null> {
        await this.ensureLogin();
        await this.getResourceTree();
        const resp = await this.httpClient.get(`/resource/file/${fileId}`, { headers: this.getAuthHeaders() });
        const data = resp?.data?.data || resp?.data;
        if (!data) return null;

        const cachedNode = this.resourceMapCache!.get(fileId);
        const type = cachedNode ? String(cachedNode.type || '') : this.inferTypeFromId(fileId);
        if (!type) return null;

        const id = String((data as any)?.id || fileId);
        const name = String((data as any)?.name || '');
        const script = String((data as any)?.script || '');
        const groupId = String((data as any)?.groupId || '');

        // Use cached fullFilePath to compute groupPath
        let fileKey = this.idToPathCache.get(id);
        let groupPath = '';
        if (cachedNode) {
            const ffp = String(cachedNode.fullFilePath || '');
            const lastSlash = ffp.lastIndexOf('/');
            const gpName = lastSlash > -1 ? ffp.substring(0, lastSlash) : '';
            groupPath = `${gpName}`;
            const ext = this.getExtForType(type as MagicResourceType);
            fileKey = `${groupPath}/${name}${ext}`;
        } else {
            // Fallback if no cached node
            const ext = this.getExtForType(type as MagicResourceType);
            fileKey = `${groupPath}/${name}${ext}`;
        }

        // Update caches
        if (id && fileKey) {
            this.idToPathCache.set(id, fileKey);
            this.pathToIdCache.set(fileKey, id);
        }

        const info: MagicFileInfo = {
            id,
            name,
            type: type as MagicResourceType,
            groupId,
            groupPath,
            path: String((data as any)?.path || ''),
            requestMapping: String((data as any)?.requestMapping || ''),
            method: String((data as any)?.method || ''),
            description: String((data as any)?.description || ''),
            script,
            createTime: (data as any)?.createTime,
            updateTime: (data as any)?.updateTime,
            createBy: (data as any)?.createBy,
            updateBy: (data as any)?.updateBy,
            params: (data as any)?.params,
            headers: (data as any)?.headers,
            contentType: (data as any)?.contentType,
            timeout: (data as any)?.timeout,
            cron: (data as any)?.cron,
            enabled: (data as any)?.enabled,
            executeOnStart: (data as any)?.executeOnStart,
            extra: (data as any)?.properties,
            raw: data,
        };
        return info;
    }

    async saveFile(file: MagicFileInfo): Promise<boolean> {
        await this.ensureLogin();
        const type = file.type;
        const payload: any = { ...file };
        // 不将 raw 透传给服务端
        if (typeof payload.raw !== 'undefined') {
            delete payload.raw;
        }
        // 发送前规范化：groupPath 仅传递分组子路径，避免服务端再次加类型导致重复
        if (payload.groupPath) {
            const segs = String(payload.groupPath).split('/').filter(Boolean);
            const typeSeg = String(type);
            while (segs[0] === typeSeg) segs.shift();
            payload.groupPath = segs.length ? segs.join('/') : undefined;
        }
        const text = JSON.stringify(payload);
        const enc = this.encodeBase64Rot13(text);
        try {
            const resp = await this.httpClient.post(`/resource/file/${type}/save`, enc, {
                headers: { ...this.getAuthHeaders(), 'Content-Type': 'text/plain;charset=UTF-8' }
            });
            const ok = !!(resp?.data?.code === 200 || resp?.data?.success === true || resp?.data?.data || resp?.data?.id);
            if (!ok) {
                const code = typeof resp?.data?.code !== 'undefined' ? resp.data.code : undefined;
                const message = resp?.data?.message ?? (typeof resp?.data === 'string' ? resp.data : undefined);
                this.setLastError(message, code);
            } else {
                this.clearLastError();
            }
            return ok;
        } catch (e: any) {
            const code = e?.response?.status ?? e?.response?.data?.code;
            const message = e?.response?.data?.message ?? e?.message;
            this.setLastError(message, code);
            return false;
        }
    }

    async createFile(request: CreateFileRequest): Promise<string | null> {
        await this.ensureLogin();
        const type = request.type;
        const payload: any = {
            ...request,
            type: type
        };
        // 发送前规范化：groupPath 仅传递分组子路径，避免服务端再次加类型导致重复
        if (payload.groupPath) {
            const segs = String(payload.groupPath).split('/').filter(Boolean);
            const typeSeg = String(type);
            while (segs[0] === typeSeg) segs.shift();
            payload.groupPath = segs.length ? segs.join('/') : undefined;
        }
        const text = JSON.stringify(payload);
        const enc = this.encodeBase64Rot13(text);
        try {
            const resp = await this.httpClient.post(`/resource/file/${type}/save`, enc, {
                headers: { ...this.getAuthHeaders(), 'Content-Type': 'text/plain;charset=UTF-8' }
            });
            const id = resp?.data?.data || resp?.data?.id || null;
            if (!id) {
                const code = typeof resp?.data?.code !== 'undefined' ? resp.data.code : undefined;
                const message = resp?.data?.message ?? (typeof resp?.data === 'string' ? resp.data : undefined);
                this.setLastError(message, code);
            } else {
                this.clearLastError();
            }
            return id;
        } catch (e: any) {
            const code = e?.response?.status ?? e?.response?.data?.code;
            const message = e?.response?.data?.message ?? e?.message;
            this.setLastError(message, code);
            return null;
        }
    }

    async deleteFile(fileId: string): Promise<boolean> {
        await this.ensureLogin();
        const body = `id=${encodeURIComponent(fileId)}`;
        const resp = await this.httpClient.post('/resource/delete', body, { headers: { ...this.getAuthHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' } });
        return !!(resp?.data?.success || resp?.data?.code === 200 || resp?.data?.data === true);
    }

    async createGroup(request: CreateGroupRequest): Promise<string | null> {
        await this.ensureLogin();
        const resp = await this.httpClient.post('/resource/folder/save', request, { headers: this.getAuthHeaders() });
        const id = resp?.data?.data || resp?.data?.id || null;
        return id;
    }

    async saveGroup(group: MagicGroupInfo): Promise<boolean> {
        await this.ensureLogin();
        const resp = await this.httpClient.post('/resource/folder/save', group, { headers: this.getAuthHeaders() });
        return !!(resp?.data?.success || resp?.data?.code === 200 || (typeof resp?.data?.data !== 'undefined'));
    }

    async deleteGroup(groupId: string): Promise<boolean> {
        await this.ensureLogin();
        const body = `id=${encodeURIComponent(groupId)}`;
        const resp = await this.httpClient.post('/resource/delete', body, { headers: { ...this.getAuthHeaders(), 'Content-Type': 'application/x-www-form-urlencoded' } });
        return !!(resp?.data?.success || resp?.data?.code === 200 || resp?.data?.data === true);
    }

    getFileIdByPath(path: string): string | undefined {
        return this.pathToIdCache.get(path);
    }

    getGroupIdByPath(path: string): string | undefined {
        return this.pathToIdCache.get(path);
    }

    getPathById(id: string): string | undefined {
        return this.idToPathCache.get(id);
    }

    // 兜底：通过遍历资源树按路径解析文件ID，并填充缓存
    async resolveFileIdByPath(fileKey: string): Promise<string | undefined> {
        try {
            await this.ensureLogin();
            // 规范化输入路径（去除前导斜杠）
            const key = String(fileKey || '').replace(/^\/+/, '');
            await this.getResourceTree();
            return this.pathToIdCache.get(key);
        } catch {
            return undefined;
        }
    }

    private buildGroupPath(group: MagicGroupInfo, allGroups: MagicGroupInfo[]): string {
        const path: string[] = [group.name];
        let parentId = group.parentId;
        while (parentId) {
            const p = allGroups.find(g => g.id === parentId);
            if (!p) break;
            path.unshift(p.name);
            parentId = p.parentId;
        }
        return path.join('/');
    }

    async getResourceDirs(): Promise<string[]> {
        await this.ensureLogin();
        const tree = await this.getResourceTree();
        const dirs: string[] = [];
        for (const type of Object.keys(tree)) {
            const root = tree[type];
            this.collectGroupDirsFromNode(type, root, [], dirs);
        }
        return dirs;
    }

    async getResourceMap() : Promise<Map<string, any>> {
        await this.getResourceTree();
        return this.resourceMapCache || new Map<string, any>();
    }
    
    async getResourceFiles(dir: string): Promise<MagicFileInfo[]> {
        await this.ensureLogin();
        const tree = await this.getResourceTree();
        const segs = dir.split('/').filter(Boolean);
        const type = segs[0];
        const root = tree[type];
        if (!root) return [];
        const groupNode = this.findGroupNodeByPath(root, segs.slice(1));
        // 目标节点集合：优先当前分组；否则在类型根与空分组下收集直系文件
        const targets: any[] = [];
        if (groupNode) {
            targets.push(groupNode);
        } else if (segs.length <= 1) {
            const typeRootId = `${type}:0`;
            const typeRootNode = this.findGroupNodeById(root, typeRootId);
            const base = typeRootNode || root;
            targets.push(base);
            // 特殊处理：跳过空分组在目录树中展示，但其下文件仍应显示在类型根
            for (const child of (base.children || [])) {
                const n = child.node || {};
                const isGroup = n && typeof n.groupId === 'undefined';
                const nameStr = String(n.name || '').trim();
                if (isGroup && nameStr.length === 0) {
                    targets.push(child);
                }
            }
        } else {
            targets.push(root);
        }

        const files: MagicFileInfo[] = [];
        for (const target of targets) {
            const children: any[] = target.children || [];
            for (const child of children) {
                const n = child.node || {};
                const isFile = n && typeof n.groupId !== 'undefined';
                if (isFile) {
                    const groupPathName = this.buildPathFromNode(root, String(n.groupId || ''));
                    const groupPath = groupPathName ? `${type}/${groupPathName}` : `${type}`;
                    const info: MagicFileInfo = {
                        id: String(n.id || ''),
                        name: String(n.name || ''),
                        type: type as MagicResourceType,
                        groupId: String(n.groupId || ''),
                        groupPath,
                        path: String(n.path || ''),
                        requestMapping: String(n.requestMapping || ''),
                        method: String(n.method || ''),
                        description: String(n.description || ''),
                        script: String(n.script || ''),
                        createTime: n.createTime,
                        updateTime: n.updateTime,
                        createBy: n.createBy,
                        updateBy: n.updateBy,
                    };
                    files.push(info);
                    if (info.id) {
                        const ext = this.getExtForType(type as MagicResourceType);
                        const fileKey = `${groupPath}/${info.name}${ext}`;
                        this.idToPathCache.set(info.id, fileKey);
                        this.pathToIdCache.set(fileKey, info.id);
                    }
                }
            }
        }
        return files;
    }

    // 新增：根据目录获取分组原始元数据（用于写入 .group-meta.json）
    async getGroupMetaByDir(dir: string): Promise<MagicGroupMetaRaw | null> {
        try {
            await this.ensureLogin();
            const tree = await this.getResourceTree();
            const segs = dir.split('/').filter(Boolean);
            const type = segs[0];
            const root = tree[type];
            if (!root) return null;
            if (segs.length <= 1) return null;
            const groupNode = this.findGroupNodeByPath(root, segs.slice(1));
            if (!groupNode) return null;
            const n = groupNode.node || {};
            const raw: MagicGroupMetaRaw = {
                properties: n.properties || {},
                id: String(n.id || ''),
                name: String(n.name || ''),
                type: (n.type || type) as MagicResourceType,
                parentId: n.parentId ? String(n.parentId) : undefined,
                path: String(n.path || ''),
                createTime: n.createTime,
                updateTime: n.updateTime,
                createBy: n.createBy,
                updateBy: n.updateBy,
                paths: n.paths || [],
                options: n.options || []
            };
            return raw;
        } catch {
            return null;
        }
    }

    getLspServerUrl(): string {
        const base = new URL(this.config.url);
        const wsProto = base.protocol === 'https:' ? 'wss' : 'ws';
        const cfgPort = this.config.lspPort;
        const basePath = base.pathname.replace(/\/$/, '');
        const cfgPrefix = basePath;
        let hostPort = base.host;
        if (cfgPort) {
            const port = String(cfgPort);
            hostPort = `${base.hostname}:${port}`;
        }
        const wsUrl = `${wsProto}://${hostPort}${cfgPrefix}/lsp`;
        debug(`LSP WS URL computed: ${wsUrl} (host=${base.hostname}, port=${cfgPort || base.port}, basePath=${basePath || '/'}, cfgPrefix=${cfgPrefix || ''})`);
        return wsUrl;
    }

    getDebugServerUrl(): string {
        const base = new URL(this.config.url);
        const wsProto = base.protocol === 'https:' ? 'wss' : 'ws';
        const port = this.config.debugPort || (base.port ? Number(base.port) : (base.protocol === 'https:' ? 443 : 80));
        const basePath = base.pathname.replace(/\/$/, '');
        const cfgPrefix = basePath;
        const hostPort = this.config.debugPort ? `${base.hostname}:${port}` : base.host;
        const wsUrl = `${wsProto}://${hostPort}${cfgPrefix}/debug`;
        debug(`Debug WS URL computed: ${wsUrl} (host=${base.hostname}, port=${port}, basePath=${basePath || '/'}, cfgPrefix=${cfgPrefix || ''})`);
        return wsUrl;
    }

    // 通过资源树收集分组目录并填充缓存
    private collectGroupDirsFromNode(type: string, node: any, pathSegs: string[], dirs: string[]): void {
        const children: any[] = node?.children || [];
        for (const child of children) {
            const n = child.node || {};
            const isGroup = n && typeof n.groupId === 'undefined';
            if (isGroup) {
                const seg = n.name;
                // 跳过空分组名，避免生成如 "datasource/" 的伪目录导致服务端重复条目
                if (!seg || String(seg).trim().length === 0) {
                    this.collectGroupDirsFromNode(type, child, pathSegs, dirs);
                    continue;
                }
                const newSegs = pathSegs.concat([seg]);
                const dirPath = `${type}/${newSegs.join('/')}`;
                dirs.push(dirPath);
                if (n.id) {
                    this.pathToIdCache.set(dirPath, n.id);
                    this.idToPathCache.set(n.id, dirPath);
                }
                this.collectGroupDirsFromNode(type, child, newSegs, dirs);
            }
        }
    }

    // 按路径段在资源树中查找分组节点
    private findGroupNodeByPath(root: any, segs: string[]): any | null {
        let current = root;
        for (const seg of segs) {
            const next = (current.children || []).find((c: any) => {
                const n = c.node || {};
                const isGroup = n && typeof n.groupId === 'undefined';
                return isGroup && n.name === seg;
            });
            if (!next) return null;
            current = next;
        }
        return current;
    }

    // 按 ID 在资源树中查找分组节点
    private findGroupNodeById(root: any, id: string): any | null {
        if (!root) return null;
        const queue: any[] = [root];
        while (queue.length) {
            const cur = queue.shift();
            const n = cur.node || {};
            const isGroup = n && typeof n.groupId === 'undefined';
            if (isGroup && n.id === id) return cur;
            const children: any[] = cur.children || [];
            for (const child of children) queue.push(child);
        }
        return null;
    }

    // 从资源树根构建指定分组的路径
    private buildPathFromNode(root: any, targetId: string): string {
        const path: string[] = [];
        const stack: any[] = [root];
        const parentMap = new Map<any, any>();
        while (stack.length) {
            const cur = stack.pop();
            const children: any[] = cur.children || [];
            for (const child of children) {
                parentMap.set(child, cur);
                stack.push(child);
            }
        }
        // DFS 找到目标节点
        const findNode = (node: any): any | null => {
            const n = node.node || {};
            if (String(n.id || '') === String(targetId || '')) return node;
            for (const child of (node.children || [])) {
                const r = findNode(child);
                if (r) return r;
            }
            return null;
        };
        const target = findNode(root);
        if (!target) return '';
        // 回溯构建路径名
        let cur: any | undefined = target;
        const nameSegs: string[] = [];
        while (cur && cur !== root) {
            const n = cur.node || {};
            const isGroup = n && typeof n.groupId === 'undefined';
            if (isGroup) nameSegs.unshift(String(n.name || ''));
            cur = parentMap.get(cur);
        }
        return nameSegs.join('/');
    }

    private inferTypeFromId(id?: string): MagicResourceType | undefined {
        if (!id) return undefined;
        if (id.startsWith('api_')) return 'api';
        if (id.startsWith('task_')) return 'task';
        if (id.startsWith('function_')) return 'function';
        if (id.startsWith('datasource_')) return 'datasource';
        return undefined;
    }

    private rot13(input: string): string {
        return input.replace(/[a-zA-Z]/g, (char) => {
            const code = char.charCodeAt(0);
            const base = code >= 97 ? 97 : 65;
            return String.fromCharCode(((code - base + 13) % 26) + base);
        });
    }

    private encodeBase64Rot13(input: string): string {
        const b64 = Buffer.from(input, 'utf-8').toString('base64');
        return this.rot13(b64);
    }

    // 解码字节为文本（尽量避免抛错）
    private tryDecodeText(bytes: Uint8Array): string | null {
        try {
            if (!bytes || bytes.length === 0) return '';
            return Buffer.from(bytes).toString('utf-8');
        } catch {
            return null;
        }
    }

  /**
   * 从缓存的资源树中获取指定文件ID对应的 fullUrlPath。
   * 若缓存尚未构建，则会优先刷新一次资源树。
   */
  public async getFullUrlPathById(id: string): Promise<string | undefined> {
    try {
      if (!id) return undefined;
      // 确保资源树缓存存在
      await this.getResourceTree(false);
      const node = this.resourceMapCache?.get(String(id));
      const full: unknown = node?.fullUrlPath;
      return (typeof full === 'string' && full.length > 0) ? full : undefined;
    } catch {
      return undefined;
    }
  }

    async getWorkbenchCompletionData(): Promise<any | null> {
        try {
            await this.ensureLogin();
            const resp = await this.httpClient.get('/workbench/completion');
            return resp?.data?.data || resp?.data || null;
        } catch {
            return null;
        }
    }

    async searchWorkbench(keyword: string): Promise<Array<{ id: string; text: string; line: number }>> {
        try {
            await this.ensureLogin();
            const resp = await this.httpClient.post('/search?keyword=' + encodeURIComponent(keyword));
            const data = resp?.data?.data || [];
            return data;
        } catch {
            return [];
        }
    }

    async downloadMirrorZip(options?: { groupId?: string; resources?: Array<{ type: MagicResourceType | 'group' | 'root'; id: string }> }): Promise<Uint8Array> {
        await this.ensureLogin();
        const payload: any = options?.resources;
        const doRequest = async () => this.httpClient.request({
            url: '/download',
            method: 'post',
            data: payload,
            responseType: 'arraybuffer',
            headers: {
                'Accept': 'application/octet-stream',
                'Content-Type': 'application/json',
            },
        });
        const resp = await doRequest();
        let buf: ArrayBuffer = resp?.data;
        let bytes = new Uint8Array(buf || new ArrayBuffer(0));
        const contentType = String(resp?.headers?.['content-type'] || '').toLowerCase();
        const looksLikeZip = bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b; // 'PK'
        const contentTypeZip = /zip|octet-stream|application\/binary/.test(contentType);
        if (!looksLikeZip || (!contentTypeZip && bytes.length > 0)) {
            debug(`下载镜像返回的内容疑似非ZIP：content-type=${contentType || 'n/a'}，尝试自动登录后重试`);
            const token = await this.ensureLogin();
            if (token) {
                const resp2 = await doRequest();
                buf = resp2?.data;
                bytes = new Uint8Array(buf || new ArrayBuffer(0));
                const contentType2 = String(resp2?.headers?.['content-type'] || '').toLowerCase();
                const looksLikeZip2 = bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
                const contentTypeZip2 = /zip|octet-stream|application\/binary/.test(contentType2);
                if (!looksLikeZip2 || (!contentTypeZip2 && bytes.length > 0)) {
                    const text = this.tryDecodeText(bytes);
                    const msg = text?.slice(0, 1000) || `${bytes.length} bytes`; // 避免过长输出
                    throw new Error(`下载镜像失败：返回的不是 ZIP（${contentType2 || 'n/a'}）。服务端响应片段：${msg}`);
                }
            } else {
                const text = this.tryDecodeText(bytes);
                const msg = text?.slice(0, 1000) || `${bytes.length} bytes`;
                throw new Error(`下载镜像失败：登录可能失效，且返回的不是 ZIP（${contentType || 'n/a'}）。响应片段：${msg}`);
            }
        }
        return bytes;
    }
}
